/* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * Created on Jan 22, 2007
 */
package cesynch.crystal.synchronizer;

import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Properties;
import java.util.Set;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.AttributeInUseException;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

/**
 * <p>Class represents a Lightweight Directory Access Protocol directory. The class
 * provides methods to manage a directory.</p>
 * 
 * @author <a href="mailto:cesynch@gmail.com">cesynch</a>
 */
public class Directory
{
    /** Properties file key for ldap url (ldap.url)*/
    public static final String KEY_LDAP_URL = "ldap.url";
    /** Properties file key for ldap protocol ssl | ldap */
    public static final String KEY_SECURITY_PROTOCOL = "ldap.protocol";
    /** Properties file key for SECURITY_AUTHENTICATION (ldap.security.authentication)*/
    public static final String KEY_SECURITY_AUTHENTICATION = "ldap.security.authentication";
    /** Properties file key for INITIAL_CONTEXT_FACTORY (ldap.initial.ctx.factory) */
    public static final String KEY_INITIAL_CONTEXT_FACTORY = "ldap.initial.ctx.factory";
    /** Properties file key for SECURITY_PRINCIPAL (ldap.security.principal) */
    public static final String KEY_SECURITY_PRINCIPAL = "ldap.security.principal";
    /** Properties file key for SECURITY_CREDENTIALS (ldap.security.credentials) */
    public static final String KEY_SECURITY_CREDENTIALS = "ldap.security.credentials";
    /** Properties file key for root bind context */
    public static final String KEY_ROOT_CTX = "ldap.root.ctx";
    /** Properties file key for the user cn */
    public static final String KEY_USERS_CN = "ldap.users.cn";  
    /** Properties file key for the group cn */
    public static final String KEY_GROUPS_CN = "ldap.groups.cn";
    /** Properties file key for the group Object type */
    public static final String KEY_GROUP_OBJ_TYPE = "ldap.group.object.type";
    /** Properties file key for the group member Object type */
    public static final String KEY_GROUP_MEM_OBJ_TYPE = "ldap.group.member.object.type";
    /** Property key for the ldap user unique attribute (normally uid) */
    public static final String KEY_USER_UNIQUE_ATTR = "ldap.users.unique.attr";
    /** Default for the ldap user unique attribute */ 
    public static final String DEFAULT_USER_UNIQUE_ATTR = "uid";
    /** Property key for the ldap jndi query batch size */
    public static final String KEY_LDAP_BATCHSIZE = "ldap.batch.size";
    /** Default for the ldap jndi query batch size */ 
    public static final String DEFAULT_LDAP_BATCHSIZE = "500";
    
    /** Constant for the directory person object class attribute */
    private static Attribute personObjClass = new BasicAttribute("objectclass");
    
    /** Constant for the directory person object class attribute */
    private static Attribute groupObjClass = new BasicAttribute("objectclass");
    
    // Static initializer for person class
    static 
    {
        personObjClass.add("top");
        personObjClass.add("person");
        personObjClass.add("organizationalPerson");
        personObjClass.add("inetorgperson");
    }
    
    // Static initializer for group class
    static 
    {
        groupObjClass.add("top");
    }

    /** String representing the ldap url (e.g. ldap://host:port) */
    private String ldapUrl;
    /** String representing the ldap protocol ssl | ldap */
    private String ldapProtocol;
    /** String representing the root bind context */
    private String rootContext;
    /** String for the user cn */
    private String userCN;
    /** String for user unique attribute */
    String userUniqueAttr;
    /** Bind user for the directory, also used as the initial member when groups are created */
    String bindUser;

    /** String for the group cn */
    private String groupCN;
    /** String group object's schema object type. */
    private String groupObjectType;
    /** String group member object's* schema object type */
    private String memberObjectType;
    /** The JNDI directory context */
    private DirContext ctx;
    /** The jndi ldap batch size setting */
    private String batchSize;
   

    /**
     * <p>Constructs a new Directory service using the given properties.</p>
     */
    public Directory(Properties p) throws Exception
    {
        super();
        
        ldapUrl = p.getProperty(KEY_LDAP_URL);
        rootContext = p.getProperty(KEY_ROOT_CTX);
        userCN = p.getProperty(KEY_USERS_CN);
        userUniqueAttr = p.getProperty(KEY_USER_UNIQUE_ATTR, DEFAULT_USER_UNIQUE_ATTR);
        groupCN = p.getProperty(KEY_GROUPS_CN);
        groupObjectType = p.getProperty(KEY_GROUP_OBJ_TYPE);
        groupObjClass.add(groupObjectType);
        memberObjectType = p.getProperty(KEY_GROUP_MEM_OBJ_TYPE);
        ldapProtocol = p.getProperty(KEY_SECURITY_PROTOCOL);
        bindUser = p.getProperty(KEY_SECURITY_PRINCIPAL);
        Hashtable env = new Hashtable();
        env.put(Context.INITIAL_CONTEXT_FACTORY, p.getProperty(KEY_INITIAL_CONTEXT_FACTORY));
        // Authenticate 
        env.put(Context.SECURITY_AUTHENTICATION, p.getProperty(KEY_SECURITY_AUTHENTICATION, "simple"));
        env.put(Context.SECURITY_PRINCIPAL, bindUser);
        env.put(Context.SECURITY_CREDENTIALS, p.getProperty(KEY_SECURITY_CREDENTIALS));
        env.put(Context.PROVIDER_URL, ldapUrl + "/" + rootContext);
        if (ldapProtocol.trim().equalsIgnoreCase("ssl"))
        {
            env.put(Context.SECURITY_PROTOCOL, "ssl");
        }
        
        ctx = new InitialDirContext(env);
        try {
            batchSize = Long.valueOf(p.getProperty(KEY_LDAP_BATCHSIZE, DEFAULT_LDAP_BATCHSIZE)).toString();

        } catch (NumberFormatException nfe) {
            batchSize = DEFAULT_LDAP_BATCHSIZE;
        }
        ctx.addToEnvironment(Context.BATCHSIZE, batchSize);
    }

    /**
     * <p></p>
     * 
     * @param uid
     * @return
     * @throws NamingException
     */
    public SearchResult findPerson(String uid) throws NamingException
    {
        Attributes matchAttrs = new BasicAttributes(true); // ignore attribute name case
        matchAttrs.put(new BasicAttribute(userUniqueAttr, uid));
        SearchResult sr = null;
        NamingEnumeration enumr = ctx.search(userCN, matchAttrs);
        if (enumr.hasMore())
        {
            sr = (SearchResult) enumr.next();
        }
        return sr;
    }

    /**
     * 
     * @param p
     * @throws NamingException
     */
    public void createPerson(Person p) throws NamingException
    {
    
        ctx.createSubcontext(userUniqueAttr + "=" + p.getUid() + ","+ userCN, personAttributes(p));
    }

    /**
     * 
     * @param p
     * @param sr
     * @throws NamingException
     */
    public void modifyPerson(Person p, SearchResult sr) throws NamingException
    {
        int i = -1;
        ModificationItem[] mods = new ModificationItem[3];
        
        mods[++i] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
                                    new BasicAttribute("cn", p.getCn()));
        mods[++i]  = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
                                    new BasicAttribute("sn", p.getSn()));
        mods[++i] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
                new BasicAttribute("givenName", p.getGivenName()));

        ctx.modifyAttributes(sr.getName() + "," + userCN, mods);
    }

    /**
     * 
     * @param p
     * @throws NamingException
     */
    public void removePerson(Person p) throws NamingException
    {
        removePerson(p.getUid());
    }

    /**
     * 
     * @param uid
     * @throws NamingException
     */
    public void removePerson(String uid) throws NamingException
    {
        ctx.destroySubcontext(userUniqueAttr + "=" + uid + "," + userCN);
    }

    /**
     * 
     * @param p
     * @return
     */
    public Attributes personAttributes(Person p)
    {
        Attributes attrs = new BasicAttributes(true);
        
        Attribute uid = new BasicAttribute(userUniqueAttr);
        Attribute givenName = new BasicAttribute("givenName");
        Attribute sn = new BasicAttribute("sn");
        Attribute cn = new BasicAttribute("cn");
        Attribute userPassword = new BasicAttribute("userPassword");
        
        uid.add(p.getUid());
        givenName.add(p.getGivenName());
        sn.add(p.getSn());
        cn.add(p.getCn());
        userPassword.add(p.getUserPassword());
        
        attrs.put(uid);
        attrs.put(givenName);
        attrs.put(sn);
        attrs.put(cn);
        attrs.put(userPassword);
        attrs.put(personObjClass);
        return attrs;
    }

    /**
     * 
     * @return
     * @throws NamingException
     */
    public Enumeration getPeople() throws NamingException
    {
        Enumeration result = null;
        Attributes matchAttrs = new BasicAttributes(true);
        matchAttrs.put(new BasicAttribute("objectclass", "inetorgperson"));
        result = ctx.search(userCN, matchAttrs);
        return result;
    }
    
    /**
     * <p></p>
     * 
     * @param group
     * @return
     * @throws NamingException
     */
    public boolean groupExists(Group group) throws NamingException
    {
        Enumeration result = null;
        Attributes matchAttrs = new BasicAttributes(true);
        matchAttrs.put(new BasicAttribute("objectclass", groupObjectType));
        matchAttrs.put(new BasicAttribute("cn", group.getName()));
        result = ctx.search(groupCN, matchAttrs);
        if (result.hasMoreElements())
        {
            return true;
        }
        return false;
    }
    
    public void createGroup(Group g) throws NamingException
    {
        
        Attributes attrs = groupAttributes(g);
        Attribute dummyUser = new BasicAttribute(memberObjectType);
        dummyUser.add(bindUser);
        attrs.put(dummyUser);
        ctx.createSubcontext("cn=" + g.getName() + ","+ groupCN, attrs);
    }
    
    public boolean groupHasMember(Person person, Group group) throws NamingException
    {
        String[] results = {memberObjectType};
        SearchControls searchCtrls = new SearchControls();
        searchCtrls.setCountLimit(1);
        searchCtrls.setReturningAttributes(results);
        String filter = "(&(objectClass=" + groupObjectType 
                           + ")(" + memberObjectType + "=" + userUniqueAttr + "=" 
                           + person.getUid() + "," 
                           + userCN + "," + rootContext + "))";

        Enumeration result = ctx.search("cn=" + group.getName() + "," + groupCN, filter, searchCtrls);
        if (result.hasMoreElements())
        {
            return true;
        }
        return false;
    }
    
    public void addGroupMember(Person person, Group group) throws NamingException
    {
        String groupFmtStr = "cn=" + group.getName() + "," + groupCN;
        String memberFmtStr = userUniqueAttr + "=" + person.getUid() + "," + userCN + "," + rootContext;
        
        BasicAttribute member = new BasicAttribute(memberObjectType);
        member.add(memberFmtStr);
        ModificationItem mod = new ModificationItem(DirContext.ADD_ATTRIBUTE, member);
        try 
        {
            ctx.modifyAttributes(groupFmtStr, new ModificationItem[]{mod});
        } 
        catch (AttributeInUseException aiue)
        {
            // Bury - ignore if user already added
        }
    }
    
    public Set findGroupsForPerson(Person person) throws NamingException
    {
        Set matchingGroups = new HashSet();
        String[] results = {"cn"};
        SearchControls searchCtrls = new SearchControls();
        searchCtrls.setReturningAttributes(results);
        String filter = "(&(objectclass=" + groupObjectType 
                           + ")(" + memberObjectType + "=" + userUniqueAttr + "=" 
                           + person.getUid() + "," 
                           + userCN + "," + rootContext + "))";
        Enumeration result = ctx.search(groupCN, filter, searchCtrls);
        Group g = null;
        if (result.hasMoreElements())
        {
            SearchResult sr = (SearchResult) result.nextElement();
            Attributes attrs = sr.getAttributes();
            g = new Group((String) attrs.get("cn").get());
            matchingGroups.add(g);
        }
        return matchingGroups;
    }
    
    public Set getGroups() throws NamingException
    {
        Set s = new HashSet();
        Enumeration result = null;
        String filter = "(objectClass=*)";
        String[] results = {"cn"};
        SearchControls controls = new SearchControls();
        controls.setReturningAttributes(results);
        controls.setCountLimit(0);
        
        result = ctx.search(groupCN, filter, controls);
        
        Group g = null;
        while (result.hasMoreElements())
        {
            SearchResult sr = (SearchResult) result.nextElement();
            Attributes attrs = sr.getAttributes();
            g = new Group((String) attrs.get("cn").get());
            s.add(g);
        }
        return s;
    }
    
    public void removeGroup(Group group) throws NamingException
    {
        String destroyGrpStr = "cn=" + group.getName() + "," + groupCN;
        ctx.destroySubcontext(destroyGrpStr);
    }
    
    public void removeGroupMember(Person person, Group group) throws NamingException
    {
        String memberFmtStr = userUniqueAttr + "=" + person.getUid() + "," + userCN + "," + rootContext;
        String groupFmtStr = "cn=" + group.getName() + "," + groupCN;
        BasicAttribute member = new BasicAttribute(memberObjectType);
        member.add(memberFmtStr);
        ModificationItem mod = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, member);

        ctx.modifyAttributes(groupFmtStr, new ModificationItem[]{mod});
    }
    
    /**
     * 
     * @param p
     * @return
     */
    public Attributes groupAttributes(Group g)
    {
        Attributes attrs = new BasicAttributes(true);
        Attribute cn = new BasicAttribute("cn");
        cn.add(g.getName());
        attrs.put(cn);
        attrs.put(groupObjClass);
        return attrs;
    }
}
